﻿namespace HDPVRIrBlasterBridge {
  using System;
  using System.Configuration;
  using System.IO.Ports;
  using System.Linq;
  
  public sealed class DirecTVSerialBlaster : IDisposable
  {
    private bool _disposed = false;
    private object _classLock = new object();
    private SerialPort _serialPort = null;

    private const byte COMMAND_PREFIX = 0xFA;
    private const byte COMMAND_DEVICE_OFF = 0x81;
    private const byte COMMAND_DEVICE_ON  = 0x82;
    private const byte COMMAND_OSD_OFF = 0x86;
    private const byte COMMAND_CHANNEL_SET = 0xA6;

    private const byte RETURN_VALID = 0xF0;
    private const byte RETURN_PROCESSING = 0xF2;
    private const byte RETURN_COMPLETE = 0xF4;

    public DirecTVSerialBlaster(string configurationFile) {
      Configuration configuration = ConfigurationManager.OpenExeConfiguration(configurationFile);

      if (configuration == null) {
        throw new ApplicationException("Unable to open the configuration file.");
      }

      if (configuration.AppSettings == null) {
        throw new ApplicationException("Unable to load the application settigns form the configuration file");
      }

      if (!configuration.AppSettings.Settings.AllKeys.Contains("comPort")) {
        throw new ApplicationException("Unable to determine the com port to use");
      }

      string comPort = configuration.AppSettings.Settings["comPort"].Value;
      if (string.IsNullOrEmpty(comPort)) {
        throw new ApplicationException("Unable to determine the com port to use");
      }

      string[] portNames = SerialPort.GetPortNames();
      if (!portNames.Contains(string.Format("COM{0}", comPort.Trim()))) {
        throw new ApplicationException(string.Format("Specified COM port does not exist: 'COM{0}'", comPort.Trim()));
      }

      int baudRate = 9600;
      if (configuration.AppSettings.Settings.AllKeys.Contains("baudRate")) {
        string baudRateValue = configuration.AppSettings.Settings["baudRate"].Value;
        if (!int.TryParse(baudRateValue, out baudRate)) {
          // Unable to determine baud rate requested, using the default of 9600
        }
      }

      int readTimeout = 1500;
      if (configuration.AppSettings.Settings.AllKeys.Contains("readTimeout")) {
        string readTimeOutValue = configuration.AppSettings.Settings["readTimeout"].Value;
        int.TryParse(readTimeOutValue, out readTimeout);
      }

      int writeTimeOut = 1500;
      if (configuration.AppSettings.Settings.AllKeys.Contains("writeTimeOut")) {
        string writeTimeOutValue = configuration.AppSettings.Settings["writeTimeOut"].Value;
        int.TryParse(writeTimeOutValue, out writeTimeOut);
      }

      this._serialPort = new SerialPort(string.Format("COM{0}", comPort), baudRate, Parity.None, 8, StopBits.One) {
        ReadTimeout = readTimeout,
        WriteTimeout = writeTimeOut
      };

      // Open the port 
      this._serialPort.Open();
    }

    ~DirecTVSerialBlaster()
    {
      Dispose(false);
    }

    private void Dispose(bool disposing)
    {
      if (!this._disposed)
      {
        if (disposing)
        {
          if (this._serialPort != null) {

            if (this._serialPort.IsOpen) {
              this._serialPort.Close();
            }

            this._serialPort.Dispose();
          }

        }

        this._disposed = true;
      }
   }

    public void Dispose()
    {
      this.Dispose(true);
      GC.SuppressFinalize(this);
    }

    public bool TurnOffDevice()
    {
      // PowerOn = 0x81

      // Obtain a lock on the serial port
      if (System.Threading.Monitor.TryEnter(this._classLock, 3000))
      {
        try {
          // Prepare the command to send to the device
          byte[] deviceCommand = new byte[] { COMMAND_PREFIX, COMMAND_DEVICE_OFF };

          // Send the command to the device
          this._serialPort.Write(deviceCommand, 0, deviceCommand.Length);

          // Wait for a response from the device
          int returnCode = this._serialPort.ReadByte();
          if ((returnCode == RETURN_VALID) || (returnCode == RETURN_PROCESSING) || (returnCode == RETURN_COMPLETE)) {
            // Command was completed with success
            return true;
          } else {
            // Command failed .. Log the reason why (returnCode)
          }
        }
        finally {
          System.Threading.Monitor.Exit(this._classLock);
        }
      }
      return false;
    }

    public bool TurnOnDevice()
    {
      // PowerOn = 0x82
      // Obtain a lock on the serial port
      if (System.Threading.Monitor.TryEnter(this._classLock, 3000)) {
        try {
          // Prepare the command to send to the device
          byte[] deviceCommand = new byte[] { COMMAND_PREFIX, COMMAND_DEVICE_ON };

          // Send the command to the device
          this._serialPort.Write(deviceCommand, 0, deviceCommand.Length);

          // Wait for a response from the device
          int returnCode = this._serialPort.ReadByte();
          if ((returnCode == RETURN_VALID) || (returnCode == RETURN_PROCESSING) || (returnCode == RETURN_COMPLETE)) {
            // Command was completed with success
            return true;
          } else {
            // Command failed .. Log the reason why (returnCode)
          }
        }
        finally {
          System.Threading.Monitor.Exit(this._classLock);
        }
      }
      return false;
    }

    public bool SetChannel(UInt32 channelNumber)
    {
      return this.SetChannel(channelNumber, false);
    }

    public bool SetChannel(UInt32 channelNumber, bool turnOnDevice)
    {
      // SetChannelNumber = 0xA6

      // Obtain a lock on the serial port
      if (System.Threading.Monitor.TryEnter(this._classLock, 3000)) {
        try {
          // Make sure the device is turned on.
          if (turnOnDevice) {
            this.TurnOnDevice();
          }

          // TODO: Turn the OSD off if it is being displayed (figure out how to do that).

          // TODO: Turn off the DTV integrated guide if it is being displayed (figure out how to do that).


          // Prepare the command to send to the device
          byte[] deviceCommand = new byte[] { COMMAND_PREFIX, COMMAND_CHANNEL_SET, (byte)(channelNumber / 256), (byte)(channelNumber % 256), 0xFF, 0xFF, 0x00 };

          // Send the command to the device
          this._serialPort.Write(deviceCommand, 0, deviceCommand.Length);

          // Wait for a response from the device
          int returnCode = this._serialPort.ReadByte();
          if ((returnCode == RETURN_VALID) || (returnCode == RETURN_PROCESSING) || (returnCode == RETURN_COMPLETE)) {
            // Command was completed with success
            return true;
          } else {
            // Command failed .. Log the reason why (returnCode)
          }
        }
        finally {
          System.Threading.Monitor.Exit(this._classLock);
        }
      }
      return false;

    }

    public UInt32 GetChannel()
    {
      // SetChannelNumber = 0x87
      return 0;
    }
  }
}
